﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using FluentAssertions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace Sharp.Interprocess.Tests
{
    [TestClass]
    public class SharedCollectionTests
    {
        private const string TEST_COLLECTION_MAP_NAME = "TestCollection";

        #region Test Object Definitions

        private class TestObjectWithId
        {
            public int Id { get; set; }

            public int Num { get; set; }

            public string Str { get; set; }

            public override int GetHashCode()
            {
                return Num.GetHashCode() ^ Str.GetHashCode() ^ Id.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                var testObj = obj as TestObjectWithId;
                return testObj != null && testObj.Id == Id;
            }
        }

        private class TestObject
        {
            public int Num { get; set; }

            public string Str { get; set; }

            public override int GetHashCode()
            {
                return Num.GetHashCode() ^ Str.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                var testObj = obj as TestObject;
                return testObj != null && testObj.Num == Num && testObj.Str == Str;
            }
        }

        #endregion

        #region Freshly Created Collection Tests
        [TestMethod]
        public void SharedCollection_NewCollection_CountPropertyIsZero()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var itemCount = sharedCollection.Count;
                itemCount.Should().Be(0);
            }
        }

        [TestMethod]
        public void SharedCollection_NewCollection_NewCollectionIsEmpty()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var sharedCollectionEmptyContents = sharedCollection.ToList();
                sharedCollectionEmptyContents.Should().NotBeNull();
                sharedCollectionEmptyContents.Count.Should().Be(0);
            }
        }

        [TestMethod]        
        public void SharedCollection_TwoCollectionsWithTheSameMapName_DifferentGenericTypes_ExceptionThrown()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                SharedCollection<string> anotherSharedCollection = null;
                this.Invoking(o => anotherSharedCollection = new SharedCollection<string>(TEST_COLLECTION_MAP_NAME, 0))
                    .ShouldThrow<ArgumentException>()
                    .WithMessage("generic parameter type of shared collection conflicts with already existing parameter type");
            }

        }

        #endregion

        #region Add Method Related Tests

        [TestMethod]
        public void SharedCollection_AddOrUpdateCalled_NewItemAdded()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME))
            {
                sharedCollection.AddOrUpdate(125);
                sharedCollection.Should().Contain(125);
            }
        }

        [TestMethod]
        public void SharedCollection_AddOrUpdateCalled_NewComplexItemAdded()
        {
            using (var sharedCollection = new SharedCollection<TestObject>(TEST_COLLECTION_MAP_NAME))
            {
                var newItem = new TestObject() { Num = 123, Str = "ABC" };
                sharedCollection.AddOrUpdate(newItem);
                sharedCollection.Should().Contain(newItem);
            }
        }

        [TestMethod]
        public void SharedCollection_AddOrUpdateCalled_ExistingComplexItemUpdated()
        {
            using (var sharedCollection = new SharedCollection<TestObjectWithId>(TEST_COLLECTION_MAP_NAME))
            {
                var item = new TestObjectWithId() { Id = 100, Num = 123, Str = "ABC" };
                var justAnItem = new TestObjectWithId() { Id = 111, Num = 456, Str = "CDE" };
                sharedCollection.Add(item);

                item.Num = 456;
                item.Str = "CDE";

                sharedCollection.AddOrUpdate(item);
                var storedItem = sharedCollection.FirstOrDefault(row => row.Id == item.Id);
                storedItem.Should().NotBeNull();
                storedItem.ShouldBeEquivalentTo(item);
            }
        }

        [TestMethod]
        public void SharedObservableCollection_AddMethodFromAnotherThread_ChangeEventFired()
        {
            using (var sharedObservableCollection = new SharedObservableCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var onChangeResetEvent = new ManualResetEventSlim();
                sharedObservableCollection.CollectionChanged += () => { onChangeResetEvent.Set(); };
                var testData = new List<int>() { 123, 234, 567, 678 };
                var addTask = new Task(() => sharedObservableCollection.AddRange(testData));
                sharedObservableCollection.MonitorEvents();
                addTask.Start();                
                Task.WaitAll(addTask);

                var fetchedData = sharedObservableCollection.ToList();
                fetchedData.Should().BeEquivalentTo(testData);

                onChangeResetEvent.Wait(sharedObservableCollection.ChangeDetectionLatency);
                sharedObservableCollection.ShouldRaise("CollectionChanged");
            }
        }

        [TestMethod]
        public void SharedCollection_AddMethodCalled_DataAdded()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var testData = new List<int>() { 123, 234, 567, 678 };
                sharedCollection.AddRange(testData);

                var fetchedData = sharedCollection.ToList();

                fetchedData.Should().BeEquivalentTo(testData);
            }
        }

        [TestMethod]
        public void SharedCollection_AddMethodCalled_NotEnoughCapacity_ShouldAutoExpandFalse_ExceptionThrown()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0, false, 1))
            {
                sharedCollection.Invoking(collectionInstance => collectionInstance.Add(123))
                                .ShouldThrow<OutOfMemoryException>()
                                .WithMessage("Not enough reserved capacity for shared collection items.");
            }
        }

        [TestMethod]
        public void SharedCollection_AddMethod_MultipleCollectionsInOneFile_DataAdded()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0,true,1000))
            {
                using (var sharedCollection2 = new SharedCollection<short>(TEST_COLLECTION_MAP_NAME, 1000))
                {
                    var testData = new List<int>() { 123, 234, 567, 678 };
                    var testData2 = new List<short>() { 555, 66, 777, 888 };
                    sharedCollection.AddRange(testData);
                    sharedCollection2.AddRange(testData2);

                    var fetchedData = sharedCollection.ToList();
                    var fetchedData2 = sharedCollection2.ToList();
                    fetchedData.Should().BeEquivalentTo(testData);
                    fetchedData2.Should().BeEquivalentTo(testData2);
                }
            }
        }

        [TestMethod]
        public void SharedCollection_AddMethodCalled_ComplexDataAdded()
        {
            using (var sharedCollection = new SharedCollection<TestObject>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var testData = new List<TestObject>()
                { 
                    new TestObject(){Num = 123 , Str = "Test1"},
                    new TestObject(){Num = 345, Str = "Test2"},
                    new TestObject(){Num = 456, Str = "Test3"}
                };

                sharedCollection.AddRange(testData);

                var fetchedData = sharedCollection.ToList();

                fetchedData.All(item => testData.Any(testItem => testItem.Str == item.Str && testItem.Num == item.Num))
                           .Should().BeTrue();
            }
        }


        [TestMethod]
        public void SharedCollection_AddMethodCalled_CountProperty_Incremented()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var itemCountBeforeAdd = sharedCollection.Count;
                sharedCollection.Add(123);
                var itemCountAfterAdd = sharedCollection.Count;

                itemCountBeforeAdd.Should().Be(0);
                itemCountAfterAdd.Should().Be(1);
            }
        }

        #endregion

        #region Remove Tests

        [TestMethod]
        public void SharedCollection_RemoveMethodCalled_ComplexItemRemoved()
        {
            using (var sharedCollection = new SharedCollection<TestObject>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var testData = new List<TestObject>()
                { 
                    new TestObject(){Num = 123 , Str = "Test1"},
                    new TestObject(){Num = 345, Str = "Test2"},
                    new TestObject(){Num = 456, Str = "Test3"}
                };

                sharedCollection.AddRange(testData);
                sharedCollection.Remove(testData[1]);
                testData.Remove(testData[1]);

                var fetchedData = sharedCollection.ToList();
                fetchedData.All(item => testData.Any(testItem => testItem.Str == item.Str && testItem.Num == item.Num))
                           .Should().BeTrue();
            }
        }

        [TestMethod]
        public void SharedCollection_RemoveMethodCalled_ItemRemoved()
        {
            using (var sharedCollection = new SharedCollection<int>(TEST_COLLECTION_MAP_NAME, 0))
            {
                var testData = new List<int>() { 123, 234, 567, 678 };

                sharedCollection.AddRange(testData);
                sharedCollection.Remove(234);
                testData.Remove(234);

                var fetchedData = sharedCollection.ToList();
                fetchedData.Should().BeEquivalentTo(testData);
            }
        }

        #endregion
    }
}
